using System.Diagnostics;
using System.Text;

namespace ServiceLib.Handler;

/// <summary>
/// Core process processing class
/// </summary>
public class CoreHandler
{
    private static readonly Lazy<CoreHandler> _instance = new(() => new());
    public static CoreHandler Instance => _instance.Value;
    private Config _config;
    private Process? _process;
    private Process? _processPre;
    private int _linuxSudoPid = -1;
    private Action<bool, string>? _updateFunc;
    private const string _tag = "CoreHandler";

    public async Task Init(Config config, Action<bool, string> updateFunc)
    {
        _config = config;
        _updateFunc = updateFunc;

        Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
        Environment.SetEnvironmentVariable(Global.XrayLocalCert, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);

        //Copy the bin folder to the storage location (for init)
        if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1")
        {
            var fromPath = Utils.GetBaseDirectory("bin");
            var toPath = Utils.GetBinPath("");
            if (fromPath != toPath)
            {
                FileManager.CopyDirectory(fromPath, toPath, true, false);
            }
        }

        if (Utils.IsNonWindows())
        {
            var coreInfo = CoreInfoHandler.Instance.GetCoreInfo();
            foreach (var it in coreInfo)
            {
                if (it.CoreType == ECoreType.v2rayN)
                {
                    if (Utils.UpgradeAppExists(out var upgradeFileName))
                    {
                        await Utils.SetLinuxChmod(upgradeFileName);
                    }
                    continue;
                }

                foreach (var name in it.CoreExes)
                {
                    var exe = Utils.GetBinPath(Utils.GetExeName(name), it.CoreType.ToString());
                    if (File.Exists(exe))
                    {
                        await Utils.SetLinuxChmod(exe);
                    }
                }
            }
        }
    }

    public async Task LoadCore(ProfileItem? node)
    {
        if (node == null)
        {
            UpdateFunc(false, ResUI.CheckServerSettings);
            return;
        }

        var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName);
        var result = await CoreConfigHandler.GenerateClientConfig(node, fileName);
        if (result.Success != true)
        {
            UpdateFunc(true, result.Msg);
            return;
        }

        UpdateFunc(false, $"{node.GetSummary()}");
        UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
        UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
        await CoreStop();
        await Task.Delay(100);

        if (Utils.IsWindows() && _config.TunModeItem.EnableTun)
        {
            await Task.Delay(100);
            await WindowsUtils.RemoveTunDevice();
        }

        await CoreStart(node);
        await CoreStartPreService(node);
        if (_process != null)
        {
            UpdateFunc(true, $"{node.GetSummary()}");
        }
    }

    public async Task<int> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
    {
        var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC) ? ECoreType.sing_box : ECoreType.Xray;
        var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
        var configPath = Utils.GetBinConfigPath(fileName);
        var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType);
        UpdateFunc(false, result.Msg);
        if (result.Success != true)
        {
            return -1;
        }

        UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
        UpdateFunc(false, configPath);

        var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
        var proc = await RunProcess(coreInfo, fileName, true, false);
        if (proc is null)
        {
            return -1;
        }

        return proc.Id;
    }

    public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem)
    {
        var node = await AppHandler.Instance.GetProfileItem(testItem.IndexId);
        if (node is null)
        {
            return -1;
        }

        var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
        var configPath = Utils.GetBinConfigPath(fileName);
        var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath);
        if (result.Success != true)
        {
            return -1;
        }

        var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
        var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);
        var proc = await RunProcess(coreInfo, fileName, true, false);
        if (proc is null)
        {
            return -1;
        }

        return proc.Id;
    }

    public async Task CoreStop()
    {
        try
        {
            if (_process != null)
            {
                await ProcUtils.ProcessKill(_process, true);
                _process = null;
            }

            if (_processPre != null)
            {
                await ProcUtils.ProcessKill(_processPre, true);
                _processPre = null;
            }

            if (_linuxSudoPid > 0)
            {
                await KillProcessAsLinuxSudo();
            }
            _linuxSudoPid = -1;
        }
        catch (Exception ex)
        {
            Logging.SaveLog(_tag, ex);
        }
    }

    #region Private

    private async Task CoreStart(ProfileItem node)
    {
        var coreType = _config.RunningCoreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
        var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(coreType);

        var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
        var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true);
        if (proc is null)
        {
            return;
        }
        _process = proc;
    }

    private async Task CoreStartPreService(ProfileItem node)
    {
        if (_process != null && !_process.HasExited)
        {
            var coreType = AppHandler.Instance.GetCoreType(node, node.ConfigType);
            var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType);
            if (itemSocks != null)
            {
                var preCoreType = itemSocks.CoreType ?? ECoreType.sing_box;
                var fileName = Utils.GetBinConfigPath(Global.CorePreConfigFileName);
                var result = await CoreConfigHandler.GenerateClientConfig(itemSocks, fileName);
                if (result.Success)
                {
                    var coreInfo = CoreInfoHandler.Instance.GetCoreInfo(preCoreType);
                    var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true);
                    if (proc is null)
                    {
                        return;
                    }
                    _processPre = proc;
                }
            }
        }
    }

    private void UpdateFunc(bool notify, string msg)
    {
        _updateFunc?.Invoke(notify, msg);
    }

    private bool IsNeedSudo(ECoreType eCoreType)
    {
        return _config.TunModeItem.EnableTun
               && eCoreType == ECoreType.sing_box
               && (Utils.IsNonWindows())
            //&& _config.TunModeItem.LinuxSudoPwd.IsNotEmpty()
            ;
    }

    #endregion Private

    #region Process

    private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
    {
        var fileName = CoreInfoHandler.Instance.GetCoreExecFile(coreInfo, out var msg);
        if (fileName.IsNullOrEmpty())
        {
            UpdateFunc(false, msg);
            return null;
        }

        try
        {
            Process proc = new()
            {
                StartInfo = new()
                {
                    FileName = fileName,
                    Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
                    WorkingDirectory = Utils.GetBinConfigPath(),
                    UseShellExecute = false,
                    RedirectStandardOutput = displayLog,
                    RedirectStandardError = displayLog,
                    CreateNoWindow = true,
                    StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
                    StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
                }
            };

            var isNeedSudo = mayNeedSudo && IsNeedSudo(coreInfo.CoreType);
            if (isNeedSudo)
            {
                await RunProcessAsLinuxSudo(proc, fileName, coreInfo, configPath);
            }

            if (displayLog)
            {
                proc.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data.IsNullOrEmpty())
                        return;
                    UpdateFunc(false, e.Data + Environment.NewLine);
                };
                proc.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data.IsNullOrEmpty())
                        return;
                    UpdateFunc(false, e.Data + Environment.NewLine);
                };
            }
            proc.Start();

            if (isNeedSudo && _config.TunModeItem.LinuxSudoPwd.IsNotEmpty())
            {
                var pwd = DesUtils.Decrypt(_config.TunModeItem.LinuxSudoPwd);
                await Task.Delay(10);
                await proc.StandardInput.WriteLineAsync(pwd);
                await Task.Delay(10);
                await proc.StandardInput.WriteLineAsync(pwd);
            }
            if (isNeedSudo)
                _linuxSudoPid = proc.Id;

            if (displayLog)
            {
                proc.BeginOutputReadLine();
                proc.BeginErrorReadLine();
            }

            await Task.Delay(500);
            AppHandler.Instance.AddProcess(proc.Handle);
            if (proc is null or { HasExited: true })
            {
                throw new Exception(ResUI.FailedToRunCore);
            }
            return proc;
        }
        catch (Exception ex)
        {
            Logging.SaveLog(_tag, ex);
            UpdateFunc(mayNeedSudo, ex.Message);
            return null;
        }
    }

    #endregion Process

    #region Linux

    private async Task RunProcessAsLinuxSudo(Process proc, string fileName, CoreInfo coreInfo, string configPath)
    {
        var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";

        var shFilePath = await CreateLinuxShellFile(cmdLine, "run_as_sudo.sh");
        proc.StartInfo.FileName = shFilePath;
        proc.StartInfo.Arguments = "";
        proc.StartInfo.WorkingDirectory = "";
        if (_config.TunModeItem.LinuxSudoPwd.IsNotEmpty())
        {
            proc.StartInfo.StandardInputEncoding = Encoding.UTF8;
            proc.StartInfo.RedirectStandardInput = true;
        }
    }

    private async Task KillProcessAsLinuxSudo()
    {
        var cmdLine = $"kill {_linuxSudoPid}";
        var shFilePath = await CreateLinuxShellFile(cmdLine, "kill_as_sudo.sh");
        Process proc = new()
        {
            StartInfo = new()
            {
                FileName = shFilePath,
                UseShellExecute = false,
                CreateNoWindow = true,
                StandardInputEncoding = Encoding.UTF8,
                RedirectStandardInput = true
            }
        };
        proc.Start();

        if (_config.TunModeItem.LinuxSudoPwd.IsNotEmpty())
        {
            try
            {
                var pwd = DesUtils.Decrypt(_config.TunModeItem.LinuxSudoPwd);
                await Task.Delay(10);
                await proc.StandardInput.WriteLineAsync(pwd);
                await Task.Delay(10);
                await proc.StandardInput.WriteLineAsync(pwd);
            }
            catch (Exception)
            {
                // ignored
            }
        }

        var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
        await proc.WaitForExitAsync(timeout.Token);
        await Task.Delay(3000);
    }

    private async Task<string> CreateLinuxShellFile(string cmdLine, string fileName)
    {
        //Shell scripts
        var shFilePath = Utils.GetBinConfigPath(AppHandler.Instance.IsAdministrator ? "root_" + fileName : fileName);
        File.Delete(shFilePath);
        var sb = new StringBuilder();
        sb.AppendLine("#!/bin/sh");
        if (AppHandler.Instance.IsAdministrator)
        {
            sb.AppendLine($"{cmdLine}");
        }
        else if (_config.TunModeItem.LinuxSudoPwd.IsNullOrEmpty())
        {
            sb.AppendLine($"pkexec {cmdLine}");
        }
        else
        {
            sb.AppendLine($"sudo -S {cmdLine}");
        }

        await File.WriteAllTextAsync(shFilePath, sb.ToString());
        await Utils.SetLinuxChmod(shFilePath);
        Logging.SaveLog(shFilePath);

        return shFilePath;
    }

    #endregion Linux
}
